iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
0
自我挑戰組

寫遊戲初體驗系列 第 23

Day 23 Entity Component System

  • 分享至 

  • xImage
  •  

Entity Component System

前言

很多遊戲引擎都使用ECS架構,所以也想寫寫看。但如果只了解理論就要從0開始時做我來真的不會,畢竟我就爛。不過還好有這篇的幫助,才讓我順利的寫(抄?)完ㄌ,之後有時間在寫寫看屬於自己的ECS架構吧XD。

開始吧

前面說過我們將遊戲物件及其行為拆成以下三個東西

  • Entity
  • Component(Data)
  • System(Logic, behavior)

每個東西還有每個東西的Manager,所以寫起來其實是很麻煩的。

Entity

Entity僅是一個簡單的ID,他不會真的存著Component或任何其他東西,事實上他只會用來作為Component陣列的索引而已

using Entity = std::uint32_t;

const Entity MAX_ENTITES = 10000;

Component

Component只會存著需要的Data,所以寫起來也很簡單

struct Transform {
    
    float x_;
    float y_;
};

struct Graphic {

    sf::Texture_;
    sf::Sprite_;
}

Component也會需要一個ID

using ComponentType = std::uint8_t;
const ComponentType MAX_COMPONENT = 32;

我們希望夠能追蹤這個Entity擁有哪些Component,同時也必須追蹤這個System會有哪些Component參與其中。

為了方便追蹤,我們會給EntitySystem一個Signature,上面登記著它擁有了哪些Component

std::bitset很適合達到我們的要求,前面我們幫每種Component訂了ID,意思是指要此物件或系統擁有這物件,就只要設定那個IDbit

舉個例子,Transform0Graphic1RigidBody2
擁有TransformRigidBody的物件,它的Signature會被設定成0b101(bit 0, 2)會被設定。

Entity Manager

Entity Manager是負責分配及追蹤Entity,記錄著哪些Entity ID已經被使用

用最簡單的std::queue就可以達成,當產生一個Entity,我們就返回一個Entity ID,當Entity被銷毀,我們再將此ID push回去就好了

class EntityManager {

public:	

    EntityManager();

    Entity createEntity();
    void destroyEntity(Entity entity);
    void setSignature(Entity entity, Signature signature);
    Signature getSignature(Entity entity);

private:

    // Array of signatures where the index is the Entity
    std::array<Signature, MAX_ENTITIES> Signatures_;

    // ALL Unused Entities
    std::queue<Entity> Entites_;

    // Total living Entites
    uint32_t EntitiesCounts_ = 0;
};

EntityManager::EntityManager() {

    // Unused Entites
    for(Entity entity = 0 ; entity < MAX_ENTITIES ; entity++)
        Entites_.push(entity);
}

Entity EntityManager::createEntity() {

    assert(EntitiesCounts_ < MAX_ENTITIES && "Too Many Entities");

    Entity id = Entites_.front();
    Entites_.pop();
    ++EntitiesCounts_;

    return id;
}

void EntityManager::destroyEntity(Entity entity) {

    assert(entity < MAX_ENTITIES && "Entity Out Of Range");

    Signatures_[entity].reset();
    Entites_.push(entity);
    --EntitiesCounts_;
}

void EntityManager::setSignature(Entity entity, Signature signature) {

    assert(entity < MAX_ENTITIES && "Entity Out Of Range");

    Signatures_[entity] = signature;
}

Signature EntityManager::getSignature(Entity entity) {

    assert(entity < MAX_ENTITIES && "Entity Out Of Range");

    return Signatures_[entity];
}

Component Array

在這裡,我們必須實作一種簡單的陣列,但必須永遠是連續的,代表說它是不會有洞出現的。

因為我們的Entity只是ID,要獲得跟Entity相關的Component是很簡單的,但是當Entity被銷毀時,對於陣列來說此Index(Entity ID)已經失效,我們不希望在遍歷陣列時出現失效的Entity

但是事實上當Entity被銷毀時,它仍然是存在在陣列裡的。

在這裡,我們維護了兩個map,存著Entity IDIndex之間的關係。當你要使用Entity時,你利用Entity ID去尋找在陣列裡實際的位置。當Entity被移除時,我們將陣列裡最後一個尚未失效的Entity移動到被移除的位置上,接著只要更新map,一切就完成。

在這裡我先掩飾給大家看看。

我們假設MAX_ENTITES5,陣列一開始是空的,map也是空的

Array 0: 1: 2: 3: 4: 5:
Entity->Index
Index->Entity
Size 0

接著我們加入Entity A, 它的Entity ID為0
在陣列中的位置是0,所以將map的對應關係寫好

Array 0: A 1: 2: 3: 4:
Entity->Index 0:0
Index->Entity 0:0
Size 1

接著加入B

Array 0: A 1: B 2: 3: 4:
Entity->Index 0:0 1:1
Index->Entity 0:0 1:1
Size 2

加入C

Array 0: A 1: B 2: C 3: 4:
Entity->Index 0:0 1:1 2:2
Index->Entity 0:0 1:1 2:2
Size 3

加入D

Array 0: A 1: B 2: C 3: D 4:
Entity->Index 0:0 1:1 2:2 3:3
Index->Entity 0:0 1:1 2:2 3:3
Size 4

到這裡都很好,陣列保持連續,但接著我們要刪除B,也就是Entity 1,為了保持連續,我們將D覆蓋B的位置然後更新map

刪除B

Array 0: A 1: D 2: C 3: 4:
Entity->Index 0:0 3:1 2:2
Index->Entity 0:0 1:3 2:2
Size 3

接著刪除Entity 3,也就是D

Array 0: A 1: C 2: 3: 4:
Entity->Index 0:0 2:1
Index->Entity 0:0 1:2
Size 2

然後我們加入E,也就是Entity 4

Array 0: A 1: C 2: E 3: 4:
Entity->Index 0:0 2:1 4:2
Index->Entity 0:0 1:2 2:4
Size 3

這樣陣列就保持連續了!!!

class IComponentArray {

public:

    virtual ~IComponentArray() = default;
    virtual void entityDestroyed(Entity entity) = 0;
};

template<typename T>
class ComponentArray: public IComponentArray {

public:
    void insertData(Entity entity, T component) {

        // assert(EntityToIndex.find(entity) == EntityToIndex.end() && "Components added to the same Entity more than once");

        size_t newIndex = size_;
        EntityToIndex[entity] = newIndex;
        IndexToEntity[newIndex] = entity;
        componentArray_[newIndex] = component;
        ++size_;
    }

    void removeData(Entity entity) {

        // assert(EntityToIndex.find(entity) != EntityToIndex.end() && "Entity does not exist");

        size_t remove = EntityToIndex[entity];
        size_t lastElement = size_-1;
        componentArray_[remove] = componentArray_[lastElement];

        Entity lastEntity = IndexToEntity[lastElement];
        EntityToIndex[lastEntity] = remove;
        IndexToEntity[remove] = lastEntity;

        EntityToIndex.erase(entity);
        IndexToEntity.erase(lastElement);

        --size_;
    }

    T& getComponent(Entity entity) {

        // assert(EntityToIndex.find(entity) != EntityToIndex.end() && "retrieving none exist component");

        return componentArray_[EntityToIndex[entity]];
    }

    void entityDestroyed(Entity entity) override {

        if(EntityToIndex.find(entity) != EntityToIndex.end())
            removeData(entity);
    }

private:

    std::array<T, MAX_ENTITIES> componentArray_;

    std::unordered_map<Entity, size_t> EntityToIndex;

    std::unordered_map<size_t, Entity> IndexToEntity;

    size_t size_ = 0;

};

這層抽象層是必要的,之後我們會有很多很多的Component array,並且用一個list存著他們,每當有Entity被銷毀時,我們必須逐一地去通知他們有Entity被銷毀了。能讓我們存各個不同type的Component的辦法就是有一層抽象層了。
當然一定有不用抽象層的方法,但這裡只是簡單實作,而且EntityDestroyed()也不是每個frame都會被呼叫。

Component Manager

Component Manager是負責管理所有的Component Array的,並不是讓他們之間溝通,而是通知他們我註冊了哪些Component,或是該刪除哪些Component

class ComponentManager {

public:
    template<typename T>
    void registerComponent() {

        const char* typeName = typeid(T).name();
        assert(componentTypes_.find(typeName) == componentTypes_.end() && "Registering component type more than once");
        componentTypes_.insert({typeName, nextComponentType_});
        componentArray_.insert({typeName, std::make_shared<ComponentArray<T>>()});

        ++nextComponentType_;
    }

    template<typename T>
    ComponentType getComponentType() {

        const char* typeName = typeid(T).name();
        assert(componentTypes_.find(typeName) != componentTypes_.end() && "Component did not register");

        return componentTypes_[typeName];
    }

    template<typename T>
    void addComponent(Entity entity, T component) {

        getComponentArray<T>()->insertData(entity, component);

    }

    template<typename T>
    void removeComponent(Entity entity) {

        getComponentArray<T>()->removeData(entity);
    }

    template<typename T>
    T& getComponent(Entity entity) {

        return getComponentArray<T>()->getComponent(entity);
    }

    void entityDestroyed(Entity entity) {

        for(auto const& pair: componentArray_) {

            auto const& component = pair.second;
            component->entityDestroyed(entity);
        }
    }


private:

    std::unordered_map<const char*, ComponentType> componentTypes_;
    std::unordered_map<const char*, std::shared_ptr<IComponentArray>> componentArray_;
    ComponentType nextComponentType_ = 0;

    template<typename T>
    std::shared_ptr<ComponentArray<T>> getComponentArray() {

        const char* typeName = typeid(T).name();
        assert(componentTypes_.find(typeName) != componentTypes_.end() && "Component did not register");

        return std::static_pointer_cast<ComponentArray<T>>(componentArray_[typeName]);

    }

};

System

System是行為存在的地方。
每個System都放著存著Entitylist

class System {

public:
    std::set<Entity> Entities_;
};

你說行為呢?

當我們需要甚麼系統就繼承它

class PhysicSystem : public System {

public:

    void update(float dt);

};

class GraphicSystem : public System {

public:

    void update();
    
    void render(sf::RenderTarget& target);
};

一個Systemupadte一般長這樣

void PhysicSystem::update(float dt) {

    for(auto const& entity : Entities_) {

        auto& transform = ecs.getComponent<Transform>(entity);
        auto& rigidBody = ecs.getComponent<RigidBody>(entity);

        transform.y_ += rigidBody.v_ * dt;

        rigidBody.v_ +=  10 * dt;
    }

}

System Manager

System Manager是負責管理所有的System及他們的Signature
每個SystemSignature代表此System會用到的Component,這樣才能將合適的Entity分配給它。
Entity被銷毀,Systemlist也得跟著更新。

class SystemManager {

public:
    template<typename T>
    std::shared_ptr<T> registerSystem() {

        const char* typeName = typeid(T).name();

        assert(system_.find(typeName) == system_.end() && "Registering system more than once");

        auto system = std::make_shared<T>();
        system_.insert({typeName, system});
        return system;
    }

    template<typename T>
    void setSignature(Signature signature) {

        const char* typeName = typeid(T).name();
        assert(system_.find(typeName) != system_.end() && "System did not register");

        signatures_.insert({typeName, signature});
    }

    void entityDestroyed(Entity entity) {

        for(auto const& pair : system_) {

            auto const& system = pair.second;
            system->Entities_.erase(entity);
        }
    }

    void entitySignatureChanged(Entity entity, Signature entitySignature) {

        for(auto const& pair: system_){

            auto const& type = pair.first;
            auto const& system = pair.second;
            auto const& systemSignature = signatures_[type];

            if((entitySignature & systemSignature) == systemSignature) system->Entities_.insert(entity);
            else system->Entities_.erase(entity);
        }
    }

private:

    std::unordered_map<const char*, Signature> signatures_;
    std::unordered_map<const char*, std::shared_ptr<System>> system_; 
};

ECS

現在我們大致上的功能都有了,我們有管理EntityEntity Manager
管理ComponentComponent Manager。管理SystemSystem Manager
這三個Manager必須互相溝通。
有很多種辦法 來處理這個問題,設定成gloabal之類的,但我這裡打算寫個class把它包起來

class ECS {

public:

    ECS() = default;

    void init();

    Entity createEntity();

    void destroyEntity(Entity entity);

    template<typename T>
    void registerComponent() {

        componentManager_->registerComponent<T>();
    }

    template<typename T>
    void addComponent(Entity entity, T component) {

        componentManager_ ->addComponent<T>(entity, component);

        auto signature = entityManager_->getSignature(entity);
        signature.set(componentManager_->getComponentType<T>(), true);
        entityManager_->setSignature(entity, signature);

        systemManager_->entitySignatureChanged(entity, signature);\
    }

    template<typename T>
    void removeComponent(Entity entity){

        componentManager_->removeComponent<T>(entity);

        auto signature = entityManager_->getSignature(entity);
        signature.set(componentManager_->getComponentType<T>(), false);
        entityManager_->setSignature(entity, signature);

        systemManager_->entitySignatureChanged(entity, signature);
    }

    template<typename T>
    T& getComponent(Entity entity) {

        return componentManager_->getComponent<T>(entity);
    }


    template<typename T>
    ComponentType getComponentType() {

        return componentManager_->getComponentType<T>();
    }

    template<typename T>
    std::shared_ptr<T> registerSystem() {

        return systemManager_->registerSystem<T>();
    }

    template<typename T>
    void setSystemSignature(Signature signature) {

        systemManager_->setSignature<T>(signature);
    }



private:

    std::unique_ptr<EntityManager> entityManager_;
    std::unique_ptr<ComponentManager> componentManager_;
    std::unique_ptr<SystemManager> systemManager_;
};


void ECS::init() {

    entityManager_ = std::make_unique<EntityManager>();
    componentManager_ = std::make_unique<ComponentManager>();
    systemManager_ = std::make_unique<SystemManager>();
    std::cout << "ECS init\n";
}


Entity ECS::createEntity() 	{

    return entityManager_->createEntity();
}

void ECS::destroyEntity(Entity entity) {

    entityManager_->destroyEntity(entity);
    componentManager_->entityDestroyed(entity);
    systemManager_->entityDestroyed(entity);
}

怎麼用

一樣,為了讓成千上萬個沙奈朵掉下來,我們一樣有三個Component

struct Transform{

public:

    float x_;
    float y_;
};


struct RigidBody{

public:

    float v_;

};


struct Graphic{

public:

    Gardevoir *gardevoir_;
    sf::Texture texture;
    sf::Sprite sprite_;
};

Gardevoir的用處在Component Pattern有提到

然後我們要來寫我們的System

我們的Graphic System

class GraphicSystem : public System {


public:

    void update();

    void setSprite(Entity entity);

    void render(sf::RenderTarget& target);
};


void GraphicSystem::update() {

    for(auto const& entity: Entities_) {

        auto& graphic = ecs.getComponent<Graphic>(entity);
        auto& transform = ecs.getComponent<Transform>(entity);

        graphic.sprite_.setPosition(transform.x_, transform.y_);
    }
}

void GraphicSystem::render(sf::RenderTarget& target) {

    for(auto const& entity : Entities_) {

        auto& graphic = ecs.getComponent<Graphic>(entity);

        target.draw(graphic.sprite_);
    }
}

void GraphicSystem::setSprite(Entity entity) {

    auto& graphic = ecs.getComponent<Graphic>(entity);
    graphic.sprite_.setTexture(graphic.gardevoir_->texture);
    graphic.sprite_.setScale(0.1, 0.1);
}

PhysicSystem

class PhysicSystem : public System {

public:

    void update(float dt);

};


void PhysicSystem::update(float dt) {

    for(auto const& entity : Entities_) {

        auto& transform = ecs.getComponent<Transform>(entity);
        auto& rigidBody = ecs.getComponent<RigidBody>(entity);

        transform.y_ += rigidBody.v_ * dt;

        rigidBody.v_ +=  10 * dt;
    }
}

接著我們的主程式我分開來講

首先我們初始化我們的ECS,並註冊我們的Component

ECS::ECS ecs;

ecs.init();
ecs.registerComponent<ECS::Transform>();
ecs.registerComponent<ECS::RigidBody>();
ecs.registerComponent<ECS::Graphic>();

接著我們註冊我們的System,記得設定Signature

auto graphicSystem = ecs.registerSystem<ECS::GraphicSystem>();
ECS::Signature signature;
signature.set(ecs.getComponentType<ECS::Graphic>());
signature.set(ecs.getComponentType<ECS::Transform>());
ecs.setSystemSignature<ECS::GraphicSystem>(signature);

auto physicSystem = ecs.registerSystem<ECS::PhysicSystem>();
signature.set(ecs.getComponentType<ECS::Transform>());
signature.set(ecs.getComponentType<ECS::RigidBody>());
ecs.setSystemSignature<ECS::PhysicSystem>(signature);

接著就可以開始產生我們的Entity

std::vector<ECS::Entity> entites(ECS::MAX_ENTITIES);

for(auto& entity: entites) {

    entity = ecs.createEntity();
    ecs.addComponent(
        entity,
        ECS::Transform{randPositionX(generator), randPositionY(generator), p}
    );
    ecs.addComponent(
        entity,
        ECS::RigidBody{20}
    );
    ecs.addComponent(
        entity,
        ECS::Graphic{gardevoir}
    );
}

for(auto& entity: graphicSystem->Entities_) {

    graphicSystem->setSprite(entity);
}

這裡利用我們的Graphic System幫我們設定圖片

接著就是Game Loop

while (running) {

    auto startTime = std::chrono::high_resolution_clock::now();

    sf::Event event;
    while (window.pollEvent(event))
    {
        // Close window: exit
        if (event.type == sf::Event::Closed)
            running = false;
    }

    physicSystem->update(dt);

    graphicSystem->update();

    window.clear(bgColor);

    graphicSystem->render(window);

    // window.draw(s2);
    window.display();
    auto stopTime = std::chrono::high_resolution_clock::now();
    dt = std::chrono::duration<float, std::chrono::seconds::period>(stopTime - startTime).count();
    std::cout << dt << "\n";
}

這樣就大功告成啦~~~

出來的效果應該跟昨天的一喔。


上一篇
Day 22 Component Pattern
下一篇
Day 24 EnTT
系列文
寫遊戲初體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言